package com.google.code.virtualhockey.vhx.util

import scala.util.parsing.combinator.RegexParsers
import scala.io.Source
import java.util.Date

/**
 * Simple CSV parser based on the following BNF grammar:
 *
 * file        ::= [ line { linebreak line } ]
 * line        ::= field ( sep field )
 * linebreak   ::= "\n" | "\r" | "\r\n"
 * field       ::= escaped | nonescaped
 * escaped     ::= ( spaces ) quote { text | linebreak | sep | doublequote } quote ( spaces )
 * nonescaped  ::= date | boolVal | floatVal | doubleVal | longVal | intVal
 * sep         ::= ";"
 * quote       ::= "\""
 * doublequote ::= "\"\""
 * spaces      ::= "[ \t]+"
 * text        ::= "[^;\"\n\r]"
 * boolVal     ::= "true" | "false"
 */
object CSVParser extends RegexParsers {

  // Whitespaces have special meaning in CSV
  //
  override def skipWhitespace = false

  // Special parsers
  //

  /** Parses a date in the format 'yyyy-MM-ddTHH:mm:ss.SSS' (e.g. '2011-06-04T18:55:00.000'). */
  def date: Parser[Date] = new Parser[Date]{
    def apply( in: Input ) = {
      val source = in.source
      val offset = in.offset
      val start = handleWhiteSpace( source, offset )
      val dateLength = LangUtils.FORMAT_STRING_DATE_CONVERSION_FOR_DISPLAY.length()

      if ( start + dateLength > source.length() ) {
        val found = source.subSequence( start, source.length() )
        Failure( "Expected date in the format '" + LangUtils.FORMAT_STRING_DATE_CONVERSION_FOR_DISPLAY + "' but found '" + found + "'.", in.drop(start - offset) )
      }
      else {
        val potentialDate = source.subSequence( start, start + dateLength ).toString
        val optDate = LangUtils.stringToDate( potentialDate )

        if ( optDate == None )
          Failure( "Expected date in the format '" + LangUtils.FORMAT_STRING_DATE_CONVERSION_FOR_DISPLAY + "' but found '" + potentialDate + "'.", in.drop( start - offset ) )
        else
          Success( optDate.get, in.drop( start + dateLength - offset ) )
      }
    }
  }

  // Literals
  //
  private def lineBreak = "\n" | "\r" | "\r\n"
  private def separator = ";"
  private def quote = "\""
  private def doubleQuote = "\"\"" ^^ { case _ => "\"" }
  private def spaces = "[ \t]+".r
  private def text = "[^;\"\n\r]".r
  private def noVal = ( "null" | "None" ) ^^ { _ => None }
  private def intVal = "-?[\\d]+".r  ^^ { _.toInt }
  private def longVal = "-?[\\d]+[lL]".r  ^^ { (s) => s.substring( 0, s.length() - 1 ).toLong }
  private def floatVal = "-?[\\d]*[.]?[\\d]+[fF]".r ^^ { (s) => s.substring( 0, s.length() - 1 ).toFloat }
  private def doubleVal = "-?[\\d]*[.]?[\\d]+[dD]".r ^^ { (s) => s.substring( 0, s.length() - 1 ).toDouble }
  private def boolVal = ( "true" | "false" ) ^^ { _.toBoolean }

  // Productions
  //
  private def file: Parser[List[List[Any]]] = repsep( line, lineBreak ) <~ ( lineBreak? )
  private def line: Parser[List[Any]] = repsep( field, separator )
  private def field: Parser[Any] = escaped | nonEscaped
  private def escaped: Parser[String] = {
    ( spaces? ) ~> quote ~> ( ( text | lineBreak | separator | doubleQuote )* ) <~ quote <~ ( spaces? ) ^^ {
      case s => s.mkString
    }
  }
  private def nonEscaped: Parser[Any] = boolVal | noVal | date | floatVal | doubleVal | longVal | intVal

  // Helper methods
  //
  def parse( s: String ) = parseAll( file, s ) match {
    case Success( res, _ ) => res
    case e                 => throw new Exception( e.toString )
  }

  def parseFile( path: String ) = parse( Source.fromFile( path, "UTF-8" ).mkString )
}
